---
title: 锁
description: 锁的划分
---

## 按照锁粒度进行划分
锁用来对数据进行锁定，我们可以从锁定对象的粒度大小来对锁进行划分，分别为行锁、页锁和表锁。

顾名思义，行锁就是按照行的粒度对数据进行锁定。锁定粒度小，发生锁冲突概率低，可以实现的并发度高，但是对于锁的开销比较大，加锁会比较慢，容易出现死锁情况。

页锁就是在页的粒度上进行锁定，锁定的资源要比行锁要多，因为一个页中可以有多个行记录。当我们使用页锁的时候，会出现数据浪费的情况，但这样的浪费最多页就是一个页上的数据行。页锁的开销介于表锁和行锁之间，会出现死锁。锁定粒度介于表锁和行锁，并发度一般。

表锁就是对数据表进行锁定，锁定粒度很大，同时发生锁冲突的概率也很低，数据访问的的并发度低。不过好处在于对锁的使用开销小，加锁会很快。

行锁、页锁和表锁是相对常见的三种锁，除此之外还可以在区和数据库的粒度上锁定数据，对应区锁和数据库锁，不同的数据库和存储引擎支持的锁粒度不同，InnoDB 和 Oracle 支持行锁和表锁。而 MyISAM 只支持表锁。

|            | 行锁 | 页锁 | 表锁 |
| ---------- | ---- | ---- | ---- |
| InnoDB     | √    |      | √    |
| MyISAM     |      |      | √    |
| Oracle     |      | √    | √    |
| SQL Server | √    | √    | √    |

每个层级的锁的数量是有限制的，因为锁会占用存储空间，锁空间的大小是有限的。当某个层级的锁数量超过了这个层级的阈值时，就会进行锁升级。锁升级就是用更大粒度的锁替代多个更小粒度的锁，比如 InnoDB 中行锁升级为 表锁。这样做的好处时占用的锁空间降低了，但同时数据的并发度也下降了。

## 从数据库管理的角度对锁进行划分

除了按照锁粒度大小对锁进行划分外，我们还可以从数据库管理的角度对锁进行划分。分为共享锁和排它锁。

共享锁也叫做读锁或 S 锁，共享锁锁定的资源可以被其它用户读取，但不能修改。在进行 SELECT 的时候，会将对象进行共享锁锁定，当数据读取完毕之后，就会释放共享锁，这样就可以保证数据在读取时不被修改。


```sql title='比如我们想给 product_comment 表上加上共享锁，可以使用下面的命令：'
LOCK table product_comment READ;
```



```sql title='当对数据库加上共享锁的时候，该数据表就变成了 只读模式，此时更新就会报错。也就是当共享锁没有释放时，不能对锁住的数据进行修改。'
mysql> UPDATE product_comment SET product_id = 10002 WHERE user_id = '912178';
ERROR 1099 (HY000): Table 'product_comment' was locked with a READ lock and can't be updated
```





```sql title='如果想要对表上的共享锁解锁，使用下面的命令即可：'
mysql> UNLOCK TABLE;
Query OK, 0 rows affected (0.00 sec)
```



```sql title='如果想要给某一行加上共享锁，比如想对 user_id = 912178 的数据行加上共享锁，命令如下：'
mysql> SELECT comment_id ,product_id ,comment_text,user_id FROM product_comment WHERE user_id = 912178 LOCK IN SHARE MODE;
+------------+------------+----------------------+---------+
| comment_id | product_id | comment_text         | user_id |
+------------+------------+----------------------+---------+
|      10001 |      10001 | b35cdab0766675de84e9 |  912178 |
|     320411 |      10001 | 1f4f043b61277114fd98 |  912178 |
+------------+------------+----------------------+---------+
2 rows in set (0.67 sec)
```

排它锁也叫做独占锁、写锁或 X 锁。排它锁锁定的数据只允许进行锁定操作的事务使用，其它事务无法对已锁定的数据进行查询或者修改。

```sql title='给 product_comment 数据表添加排他锁,这时只有获得排它锁的事务可以对 product_comment 进行查询和修改，其它事务如果想要在 product_comment 表上查询数据，则需要等待。'
mysql> LOCK TABLE product_comment WRITE;
Query OK, 0 rows affected (0.01 sec)
```



```sql title='释放掉排它锁
UNLOCK TABLE;
```



```sql title='同样，如果要在某个数据行上添加排它锁，比如针对 user_id = 912178 的数据行，则写成如下这样：'
mysql> SELECT comment_id,product_id,comment_text,user_id FROM product_comment WHERE user_id = 912178 FOR UPDATE;
+------------+------------+----------------------+---------+
| comment_id | product_id | comment_text         | user_id |
+------------+------------+----------------------+---------+
|      10001 |      10002 | b35cdab0766675de84e9 |  912178 |
|     320411 |      10002 | 1f4f043b61277114fd98 |  912178 |
+------------+------------+----------------------+---------+
2 rows in set (0.67 sec)
```

另外当我们对数据行进行更新的时候，也就是 INSERT、DELETE 或者 UPDATE 的时候，数据库也会自动使用排它锁，防止其他事务对该数据进行操作。

当我们想要获取某个数据表的排它锁的时候，需要先看下这张数据表有没有上了排它锁。如果这个数据表中的某个数据行上了排它锁，我们就无法获取排它锁。这时需要对数据表中的行逐一检查，检查是否有行锁，如果没有，才可以获取这张数据表的排它锁。

## 从看待并发的思维方式进行划分

从看待并发的思维方式划分的话，可以将锁分成乐观锁和悲观锁。

乐观锁认为对同一数据的并发操作不会总发生，属于小概率事件，不用每次都上锁，也就是不采用数据库自身的锁机制，而是通过程序来实现。

在程序上，我们可以采用版本号机制或者时间戳实现。

### 乐观锁的版本号机制
在表中设计一个版本字段 version,第一次读取的时候，会获取 version 字段的取值。然后对数据进行更新或删除操作时，会执行 UPDATE ... SET version = version + 1 WHERE version = version。
此时如果已经有事务对这条数据进行了更改，修改就不回成功。

这种方式类似我们熟悉的 SVN 、CVS 版本管理系统，当我们修改了代码进行提交时，首先会检查当前版本号与服务器上的版本号是否一致，如果一致就可以直接提交，如果不一致就需要更新服务器上的最新大妈，然后再进行提交。

### 乐观锁的时间戳机制
时间戳和版本号机制一样，也是在更新提交的时候，将当前数据的时间戳和更新之前取得的时间戳进行比较，如果两者一致则更新成功，否则就是版本号冲突。

乐观锁是程序员自己控制数据并发操作的权限，基本是通过给数据行增加一个戳（版本号或者时间戳），从而证明当前拿到的数据是最新。

悲观锁也是一种思想，对数据被其它事务修改持悲观态度，会通过数据库自身的锁机制来实现，从而保证数据操作的排它性。

![0dd602515faab3b6b0cd0d42adaa87e3](https://img.wkq.pub/pic/0dd602515faab3b6b0cd0d42adaa87e3.webp)





从两种锁的设计思想中，可以看出乐观锁和悲观锁的适用场景：

1. 乐观锁适合读操作多的场景，相对来说写操作比较少。它的优点在于程序实现，不存在死锁问题，不过适用场景也会相对乐观，因为它阻止不了除程序以外的数据库操作。
2. 悲观锁适合写操作多的场景，因为写的操作具有排它性。采用悲观锁的方式，可以在数据库层面阻止其他事务对该数据的操作权限，防止读——写和写——写的冲突。

